﻿using System;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.UI.HtmlControls;
using System.ComponentModel;
using System.Drawing;
using System.Web.UI.Design;
using System.Collections;
using System.Collections.Specialized;
using System.Configuration;
using System.Text;
using System.Web;
using System.Security.Permissions;

using AjaxControlToolkit;

using BizElements.BusinessLayer;

namespace BizElements.Web
{
    #region Documentation.
    /// <summary>
    /// Lookup control bound to a <see cref="BizElements.BusinessLayer.IBindableField"/> business class property
    /// which includes at least one displayable field (see <see cref="BizElements.BusinessLayer.IBindableField.SatelliteFields"/>).
    /// </summary>
    /// <remarks><para><b>BoundAutoComplete</b> is used to select a key-value pair from a list of items dynamically populated
    /// by AJAX <b>AutoCompleteExtender</b> control.</para>
    /// <para>Tipically the lookup control is bound, similarly to list controls such as DropDownList,
    /// to a single key property (set and retrieved by <see cref="SelectedValue"/> property) which is hidden from 
    /// the user. The <see cref="Text"/> property of the <b>BoundAutoComplete</b> shows a display member
    /// (see <see cref="BizElements.BusinessLayer.IBindableField.SatelliteFields"/>). Eg. when selecting a country 
    /// from a flyout menu/list, the <see cref="SelectedValue"/> property will contain <b>CountryId</b> value and the 
    /// <see cref="Text"/> property will contain <b>CountryName</b>.</para>
    /// </remarks>
    #endregion
    [ToolboxData("<{0}:BoundAutoComplete runat=server></{0}:BoundAutoComplete>")]
    [DefaultEvent("ValueSelected")]
    public class BoundAutoComplete : WebControl, INamingContainer, ISearchClient, IBoundControl, IValidator, IBoundLookupControl
    {
        #region Child controls.

        AutoCompleteExtender uiAutoComplete = new AutoCompleteExtender();
        HiddenField hdnValue = new HiddenField();
        TextBox txtText = new TextBox();
        Label lblText = new Label();
        HtmlImage imgClear = new HtmlImage();
        Label lblErrorIndicator = new Label();
        Literal uiBr = new Literal();
        Label lblError = new Label();
        Button btnAutoPostBack = new Button();

        #endregion

        #region CTor and CreateChildControls.

        /// <summary>
        /// Initializes a new instance of the <b>BoundAutoComplete</b> class.
        /// </summary>
        public BoundAutoComplete()
        {
            this.hdnValue.ID = "hdnValue";

            this.txtText.ID = "txtText";
            this.txtText.AutoCompleteType = AutoCompleteType.Disabled;

            this.lblText.ID = "lblText";
            this.lblText.EnableViewState = false;
            this.lblText.Visible = false;

            this.imgClear.ID = "imgClear";
            this.imgClear.Style["cursor"] = "pointer";
            this.imgClear.EnableViewState = false;

            this.uiAutoComplete.ID = "uiAutoComplete";
            this.uiAutoComplete.FirstRowSelected = true;            
            this.uiAutoComplete.DelimiterCharacters = "";
            this.uiAutoComplete.MinimumPrefixLength = 1;            

            this.lblErrorIndicator.ID = "lblErrorIndicator";
            this.lblErrorIndicator.Text = "*";
            this.lblErrorIndicator.ForeColor = Color.Red;
            this.lblErrorIndicator.EnableViewState = false;
            this.lblErrorIndicator.Visible = false;

            this.uiBr.Text = "<br />";
            this.uiBr.EnableViewState = false;

            this.lblError.ID = "lblError";
            this.lblError.ForeColor = Color.Red;
            this.lblError.EnableViewState = false;

            this.btnAutoPostBack.Style["display"] = "none";
        }

        /// <summary>
        /// Called by the ASP.NET page framework to notify server controls that use composition-based 
        /// implementation to create any child controls they contain in preparation for posting back or rendering. 
        /// </summary>
        protected override void CreateChildControls()
        {
            this.Controls.Clear();
            this.Controls.Add(this.uiAutoComplete);
            this.Controls.Add(this.hdnValue);
            this.Controls.Add(new LiteralControl("<div style=\"white-space:nowrap\">"));
            this.Controls.Add(this.txtText);
            this.Controls.Add(this.lblText);
            this.Controls.Add(this.imgClear);
            this.Controls.Add(this.lblErrorIndicator);
            this.Controls.Add(new LiteralControl("</div>"));
            this.Controls.Add(this.uiBr);
            this.Controls.Add(this.lblError);
            this.Controls.Add(this.btnAutoPostBack);
        }

        #endregion        

        #region OnPreRender - register scripts.

        // OldText variable is initialized with current text.
        const string OldTextVarNameFormat = "{0}_OldText";
        const string OldTextVarDeclarationFormat = "var {0}_OldText = \"{1}\"; ";

        // IsDirty flag is initially set to false. It indicates that current text hasn't been selected from the list of available text values.
        const string IsDirtyVarNameFormat = "{0}_IsDirty";
        const string IsDirtyVarDeclarationFormat = "var {0}_IsDirty = false; ";

        // MarkDirty function handles keypress events of text controls. Sets IsDirty flag if text has changed.
        const string MarkDirtyIfChangedOrResetIfClearedFunctionCallFormat = "{0}_MarkDirtyIfChangedOrResetIfCleared(this, event); ";
        const string MarkDirtyIfChangedOrResetIfClearedFunctionBodyFormat = 
            "function {0}_MarkDirtyIfChangedOrResetIfCleared(source, eventArgs){{" +
            " var newText = source.value;" +
            " if (newText == null || newText == '') {{" +
            "  {0}_ResetAll();" +
            " }} else if (newText != {0}_OldText) {{" +
            "  {0}_IsDirty = true;" +
            " }}" +
            "}} ";

        // SyncValues resets IsDirty flag, sets old text to match new text and uses Web service to get key value for the selected item.
        const string SyncValuesFunctionNameFormat = "{0}_SyncValues";
        const string SyncValuesFunctionBodyFormat =
            "function {0}_SyncValues(source, eventArgs){{" +
            " var currentPrefix = $get(\"{1}\").value;" +
            " {0}_OldText = currentPrefix;" +
            " {0}_IsDirty = false;" +
            " {0}_GetKeyValue(currentPrefix);" +
            "}} ";

        // GetKeyValue uses Web service to fetch key value that matches entered/selected text/display value.
        const string GetKeyValueFunctionBodyFormat =
            "function {0}_GetKeyValue(currentPrefix){{" +
            " var wsParams = {{ prefixText: currentPrefix, contextKey: \"{1}\" }};" +
            " Sys.Net.WebServiceProxy.invoke(\"{2}\", \"{3}\", false, wsParams, Function.createDelegate(this, {0}_OnGetKeyValueComplete));" +
            "}} ";

        // Sets the key value retrieved from Web service to hidden hdnValue control. Called asynchronously. Optionally auto-postbacks.
        const string OnGetKeyValueCompleteFunctionBodyFormat =
            "function {0}_OnGetKeyValueComplete(keyValue){{" +
            " $get(\"{1}\").value = keyValue;" +
            " {0}_AutoPostBack();" +
            "}} ";

        // Clear/reset all values and flags if user leaves the control while in dirty state.
        const string ResetAllIfDirtyFunctionCallFormat = "{0}_ResetAllIfDirty(this, event);"; 
        const string ResetAllIfDirtyFunctionBodyFormat =
            "function {0}_ResetAllIfDirty(source, eventArgs){{" +
            " if ({0}_IsDirty) {{" +
            "  {0}_ResetAll();" +
            " }}" +
            "}} ";

        // Clears all values. Optionally auto-postbacks.
        const string ResetAllFunctionCallFormat = "{0}_ResetAll();";
        const string ResetAllFunctionBodyFormat =
            "function {0}_ResetAll(){{" +
            " $get(\"{1}\").value = '';" +
            " $get(\"{2}\").value = '';" +
            " {0}_OldText = '';" +
            " {0}_IsDirty = false;" +
            " {0}_AutoPostBack();" +
            "}} ";

        // Try to find key-value pair based on partially entered prefix, ie. it is not selected from the flyout menu. Occurs onblur.
        const string TrySyncIfDirtyFunctionCallFormat = "{0}_TrySyncIfDirty(this, event);";
        const string TrySyncIfDirtyFunctionBodyFormat =
            "function {0}_TrySyncIfDirty(source, eventArgs){{" +
            " if ({0}_IsDirty) {{" +
            "  var currentPrefix = $get(\"{1}\").value;" +
            "  if (currentPrefix == null || currentPrefix == ''){{" +
            "   {0}_ResetAll();" +
            "  }} else {{" +
            "   {0}_GetKeyAndDisplayValue(currentPrefix);" +
            "  }}" +
            " }}" +
            "}} ";

        // GetKeyAndDisplayValue uses Web service to fetch key and complete display value that matches entered display value prefix.
        const string GetKeyAndDisplayValueFunctionBodyFormat =
            "function {0}_GetKeyAndDisplayValue(currentPrefix){{" +
            " var wsParams = {{ prefixText: currentPrefix, contextKey: \"{1}\" }};" +
            " Sys.Net.WebServiceProxy.invoke(\"{2}\", \"{3}\", false, wsParams, Function.createDelegate(this, {0}_OnGetKeyAndDisplayValueComplete));" +
            "}}";

        // Sets key and display values retrieved from Web service to hidden hdnValue and visible txtText controls. Called asynchronously. Optionally auto-postbacks.
        const string OnGetKeyAndDisplayValueCompleteFunctionBodyFormat =
            "function {0}_OnGetKeyAndDisplayValueComplete(arrKeyAndDisplayValue){{" +
            " if (arrKeyAndDisplayValue != null && arrKeyAndDisplayValue.length >= 2){{" +
            "  $get(\"{1}\").value = arrKeyAndDisplayValue[0];" +
            "  $get(\"{2}\").value = arrKeyAndDisplayValue[1];" +            
            "  {0}_IsDirty = false;" +
            "  {0}_AutoPostBack();" +
            " }} else {{" +
            "  {0}_ResetAll();" +
            " }}" +
            "}} ";

        // "Clicks" auto-postback button if auto-postback is enabled.
        const string AutoPostBackFunctionBodyFormat = "function {0}_AutoPostBack(){{" + 
            " if ({1}) {{" +
            "  $get(\"{2}\").click();" +
            " }}" +
            "}}";

        /// <summary>
        /// Registers required client-side script and raises the <b>PreRender</b> event.
        /// </summary>
        /// <param name="e">An <see cref="EventArgs"/> object that contains the event data.</param>
        protected override void OnPreRender(EventArgs e)
        {
            if (string.IsNullOrEmpty(this.SearchProviderKey))
                throw new InvalidOperationException("SearchProviderKey is not defined.");
            if (string.IsNullOrEmpty(this.ServicePath))
                throw new InvalidOperationException("ServicePath is not defined.");
            if (string.IsNullOrEmpty(this.GetKeyValueServiceMethod))
                throw new InvalidOperationException("GetKeyValueServiceMethod is not defined.");

            this.uiAutoComplete.TargetControlID = this.txtText.ID;
            base.OnPreRender(e);

            StringBuilder scripts = new StringBuilder();            
            // OldText variable is initialized with current text.
            scripts.AppendFormat(OldTextVarDeclarationFormat, this.ClientID, JScript.CleanString(this.txtText.Text));
            scripts.AppendLine();
            // IsDirty flag is initially set to false. It indicates that current text hasn't been selected from the list of available text values.
            scripts.AppendFormat(IsDirtyVarDeclarationFormat, this.ClientID);
            scripts.AppendLine();
            // MarkDirty function handles keypress events of text controls. Sets IsDirty flag if text has changed.
            scripts.AppendFormat(MarkDirtyIfChangedOrResetIfClearedFunctionBodyFormat, this.ClientID);
            scripts.AppendLine();
            // SyncValues resets IsDirty flag, sets old text to match new text and uses Web service to get key value for the selected item.
            scripts.AppendFormat(SyncValuesFunctionBodyFormat, this.ClientID, this.txtText.ClientID);
            scripts.AppendLine();
            // GetKeyValue uses Web service to fetch key value that matches entered/selected text/display value.
            scripts.AppendFormat(GetKeyValueFunctionBodyFormat, this.ClientID, this.SearchProviderKey, ResolveClientUrl(this.ServicePath), this.GetKeyValueServiceMethod);
            scripts.AppendLine();
            // Sets the key value retrieved from Web service to hidden hdnValue control. Called asynchronously. Optionally auto-postbacks.
            scripts.AppendFormat(OnGetKeyValueCompleteFunctionBodyFormat, this.ClientID, this.hdnValue.ClientID);
            scripts.AppendLine();
            // Clear/reset all values and flags if user leaves the control while in dirty state.
            scripts.AppendFormat(ResetAllIfDirtyFunctionBodyFormat, this.ClientID);
            scripts.AppendLine();
            // Clears all values. Optionally auto-postbacks.
            scripts.AppendFormat(ResetAllFunctionBodyFormat, this.ClientID, this.hdnValue.ClientID, this.txtText.ClientID);
            scripts.AppendLine();
            // "Clicks" auto-postback button if auto-postback is enabled.
            string jsShouldAutoPostBack = (this.AutoPostBack) ? "true" : "false";
            scripts.AppendFormat(AutoPostBackFunctionBodyFormat, this.ClientID, jsShouldAutoPostBack, this.btnAutoPostBack.ClientID);
            scripts.AppendLine();
            
            if (!string.IsNullOrEmpty(this.GetKeyAndDisplayValueServiceMethod))
            {
                // Try to find key-value pair based on partially entered prefix, ie. it is not selected from the flyout menu. Occurs onblur.
                scripts.AppendFormat(TrySyncIfDirtyFunctionBodyFormat, this.ClientID, this.txtText.ClientID);
                scripts.AppendLine();
                // GetKeyAndDisplayValue uses Web service to fetch key and complete display value that matches entered display value prefix.
                scripts.AppendFormat(GetKeyAndDisplayValueFunctionBodyFormat, this.ClientID, this.SearchProviderKey, ResolveClientUrl(this.ServicePath), this.GetKeyAndDisplayValueServiceMethod);
                scripts.AppendLine();
                // Sets key and display values retrieved from Web service to hidden hdnValue and visible txtText controls. Called asynchronously. Optionally auto-postbacks.
                scripts.AppendFormat(OnGetKeyAndDisplayValueCompleteFunctionBodyFormat, this.ClientID, this.hdnValue.ClientID, this.txtText.ClientID);
                scripts.AppendLine();
            }

            JScript.RegisterClientScriptBlock(scripts.ToString(), true);
        }

        #endregion

        #region Render - define clientside event handles, render error, read-only mode.

        /// <summary>
        /// Renders the control to the specified HTML writer.
        /// </summary>
        /// <param name="writer">The <see cref="System.Web.UI.HtmlTextWriter"/> object that receives the control content.</param>
        protected override void Render(HtmlTextWriter writer)
        {
            if (!DesignMode)
            {
                this.uiAutoComplete.OnClientItemSelected = string.Format(SyncValuesFunctionNameFormat, this.ClientID);
                this.txtText.Attributes["onkeyup"] = string.Format(MarkDirtyIfChangedOrResetIfClearedFunctionCallFormat, this.ClientID);                
                this.imgClear.Attributes["onclick"] = string.Format(ResetAllFunctionCallFormat, this.ClientID);
                if (string.IsNullOrEmpty(this.GetKeyAndDisplayValueServiceMethod))
                    this.txtText.Attributes["onblur"] = string.Format(ResetAllIfDirtyFunctionCallFormat, this.ClientID);
                else
                    this.txtText.Attributes["onblur"] = string.Format(TrySyncIfDirtyFunctionCallFormat, this.ClientID);
            }
            
            bool inReadOnlyMode = BoundControlHelper.IsInReadOnlyMode(this.AccessMode, this.BusinessField);
            ApplyAccessMode(inReadOnlyMode);

            RenderErrorMesage();
            base.Render(writer);
        }

        private void ApplyAccessMode(bool inReadOnlyMode)
        {
            // Can't use "Visible = false" because we want Text property encapsulates txtText.Text property and depends on TextBox's ViewState implementation.
            this.txtText.Style["display"] = (inReadOnlyMode) ? "none" : "";
            this.lblText.Text = this.Text;
            this.lblText.Visible = inReadOnlyMode;
            if (inReadOnlyMode)
            {
                this.imgClear.Visible = false;
            }
        }

        private void RenderErrorMesage()
        {
            bool noErrors = string.IsNullOrEmpty(this.ErrorText);
            if (noErrors)
            {
                this.lblError.Visible = this.uiBr.Visible = this.lblErrorIndicator.Visible = false;
            }
            else
            {
                switch (this.ErrorLocation)
                { 
                    case ErrorLocation.Below:                        
                        this.lblErrorIndicator.Visible = false;
                        this.uiBr.Visible = true;
                        this.lblError.Visible = true;
                        break;

                    case ErrorLocation.Right:
                        this.lblErrorIndicator.Visible = false;
                        this.uiBr.Visible = false;
                        this.lblError.Visible = true;
                        break;

                    case ErrorLocation.ValidationSummary:
                        this.lblErrorIndicator.Visible = true;
                        this.uiBr.Visible = false;
                        this.lblError.Visible = false;
                        break;

                    default:
                        // Same as below.
                        this.lblErrorIndicator.Visible = false;
                        this.uiBr.Visible = true;
                        this.lblError.Visible = true;
                        break;
                }
            }
        }

        #endregion

        #region ISearchClient members - only SearchProviderKey.

        /// <summary>
        /// Not supported. Returns <b>null</b>.
        /// </summary>
        IWebCommand ISearchClient.CreateAcceptCommand(bool calledFromPopupWindow, string text, string[] requiredParameters)
        {
            return null;
        }

        /// <summary>
        /// Not supported. Returns <b>null</b>.
        /// </summary>
        IWebCommand ISearchClient.CreateCancelCommand(bool calledFromPopupWindow, string text)
        {            
            return null;
        }

        /// <summary>
        /// Gets or sets the key under which the search provider is registered in <see cref="SearchProviderRegistry"/>.
        /// </summary>
        [Bindable(false)]
        [Category("Behavior")]
        [DefaultValue(typeof(string), null)]
        [Description("Key under which the search provider is registered in SearchProviderRegistry.")]
        public string SearchProviderKey
        {
            get { return this.uiAutoComplete.ContextKey; }
            set { this.uiAutoComplete.ContextKey = value; }
        }

        #endregion

        #region IBoundControl.

        FieldAccessMode accessMode = FieldAccessMode.Automatic;

        /// <summary>
        /// Gets or sets the value which indicates whether the <b>BoundAutoComplete</b> can change data in the bound field.
        /// </summary>
        [Bindable(false)]
        [Category("Behavior")]
        [DefaultValue(typeof(FieldAccessMode), "Automatic")]
        [Description("Indicates whether the contents of the control can be changed.")]
        public FieldAccessMode AccessMode
        {
            get { return this.accessMode; }
            set { this.accessMode = value; }
        }

        IBindableField bizField;

        /// <summary>
        /// Gets or sets the business class' property to which the control is bound. Set programmatically.
        /// </summary>
        [Description("Business class' property to which the control is bound. Set programmatically.")]
        [Browsable(false)]
        [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
        public virtual IBindableField BusinessField
        {
            get { return this.bizField; }
            set { this.bizField = value; }
        }

        /// <summary>
        /// Appends all errors in the collection to the <see cref="ErrorText"/>.
        /// </summary>
        /// <param name="rules">Collection of broken rules.</param>
        /// <param name="formatter">The <see cref="IMessageFormatter"/> object used to format the messages contained in the collection.</param>
        public virtual void AppendError(BusinessRuleCollection rules, IMessageFormatter formatter)
        {
            BusinessRuleCollection brokenRulesForThisControl = rules.GetRulesForField(this.BusinessField.BindingName, true);
            this.ErrorText += brokenRulesForThisControl.ToString(formatter) + " ";
        }

        /// <summary>
        /// Clears errors. Same as setting the <see cref="ErrorText"/> to empty string.
        /// </summary>
        public virtual void ClearError()
        {
            this.ErrorText = "";
        }

        #endregion

        #region Error label properties.

        /// <summary>
        /// Gets or sets the CSS Class name applied to the error label.
        /// </summary>
        [Bindable(false)]
        [Category("Appearance")]
        [DefaultValue(null)]
        [Description("CSS Class name applied to the error label.")]
        public virtual string ErrorCss
        {
            get { return this.lblError.CssClass; }
            set { this.lblError.CssClass = this.lblErrorIndicator.CssClass = value; }
        }

        /// <summary>
        /// Gets or sets the color of the error text.
        /// </summary>
        [Bindable(false)]
        [Category("Appearance")]
        [DefaultValue(typeof(Color), "Red")]
        [Description("Color of the error text.")]
        [TypeConverter(typeof(WebColorConverter))]
        public virtual Color ErrorColor
        {
            get { return this.lblError.ForeColor; }
            set { this.lblError.ForeColor = this.lblErrorIndicator.ForeColor = value; }
        }

        /// <summary>
        /// Gets or sets the error text displayed in the error label or in the page's validation summary. 
        /// Usually set programmatically.
        /// </summary>
        [Bindable(false), Category("Appearance"),
        Description("Error text displayed in the error label or in the page's validation summary. Usually set programmatically.")]
        [DefaultValue("")]
        public virtual string ErrorText
        {
            get { return this.lblError.Text ?? ""; }
            set { this.lblError.Text = value ?? ""; }
        }

        /// <summary>
        /// Gets or sets the indicator placed by the control if it contains an invalid value. 
        /// Displayed only when the error text is displayed in the validation summary.
        /// </summary>
        [Bindable(false)]
        [Category("Appearance")]
        [DefaultValue("*")]
        [Description("Indicator placed by the control if it contains an invalid value. Displayed only when the error text is displayed in the validation summary.")]
        public virtual string ErrorIndicator
        {
            get { return this.lblErrorIndicator.Text ?? ""; }
            set { this.lblErrorIndicator.Text = value ?? ""; }
        }

        ErrorLocation errorLocation = ErrorLocation.Below;

        /// <summary>
        /// Gets or sets the value which specifies where the error text is located.
        /// </summary>
        [Bindable(false)]
        [Category("Appearance")]
        [DefaultValue(typeof(ErrorLocation), "Below")]
        [Description("Specifies where the error text is located.")]
        public virtual ErrorLocation ErrorLocation
        {
            get { return this.errorLocation; }
            set { this.errorLocation = value; }
        }

        #endregion

        #region IValidator.

        /// <summary>
        /// Gets the value contained in the <see cref="ErrorText"/> property if a validation summary has been specified as location for errors.
        /// </summary>
        string IValidator.ErrorMessage
        {
            get { return (this.ErrorLocation == ErrorLocation.ValidationSummary) ? this.ErrorText : ""; }
            set { }
        }

        /// <summary>
        /// Does nothing. Validation logic is performed elsewhere (eg. business class).
        /// </summary>
        void IValidator.Validate()
        {
        }

        /// <summary>
        /// Gets a value which indicates whether an the <see cref="ErrorText"/> property contains error description.
        /// </summary>
        bool IValidator.IsValid
        {
            get { return string.IsNullOrEmpty(this.ErrorText); }
            set { }
        }

        #endregion

        #region AutoCompleteExtender Properties.
        
        /// <summary>
        /// Gets or sets the path to the web service that the control will pull the word completions from and fetch key values.
        /// </summary>
        /// <remarks>The service must expose the methods defined by <see cref="GetCompletionListServiceMethod"/> 
        /// and <see cref="GetKeyValueServiceMethod"/> properties.</remarks>
        [Bindable(false)]
        [Category("Behavior")]
        [DefaultValue(null)]
        [Description("The path to the web service that the control will pull the word completions from.")]
        public string ServicePath
        {
            get { return this.uiAutoComplete.ServicePath; }
            set { this.uiAutoComplete.ServicePath = value; }
        }

        /// <summary>
        /// Gets or sets the web service method to be called when a user types some text and which returns word completions.
        /// </summary>
        /// <remarks>The signature of the Web method must match the following:
        /// <code>public string[] GetCompletionList(string prefixText, int count)</code>
        /// Note that you can replace "GetCompletionList" with a name of your choice, but the return type and parameter name 
        /// and type must exactly match, including case.
        /// </remarks>
        [Bindable(false)]
        [Category("Behavior")]
        [DefaultValue(null)]
        [Description("The web service method to be called when a user types some text and which returns word completions.")]
        public string GetCompletionListServiceMethod
        {
            get { return this.uiAutoComplete.ServiceMethod; }
            set { this.uiAutoComplete.ServiceMethod = value; }
        }        

        string getKeyValueServiceMethod;

        /// <summary>
        /// Gets or sets the web service method to be called when a user selects an item form the flyout list/menu and which returns a key value that matches entered/selected text/display value.
        /// </summary>
        /// <remarks>The signature of this method must match the following:
        /// <code>public string GetKeyValue(string prefixText, string contextKey)</code>
        /// Note that you can replace "GetKeyValue" with a name of your choice, but the return type and parameter name 
        /// and type must exactly match, including case.
        /// </remarks>
        [Bindable(false)]
        [Category("Behavior")]
        [DefaultValue(null)]
        [Description("The web service method to be called when a user selects an item form the flyout list/menu and which returns a key value that matches entered/selected text/display value.")]
        public string GetKeyValueServiceMethod
        {
            get { return getKeyValueServiceMethod; }
            set { getKeyValueServiceMethod = value; }
        }

        string getKeyAndDisplayValueServiceMethod;        

        /// <summary>
        /// Gets or sets optional web service method to be called when a user types in a value prefix but doesn't wait for flyout list/menu to be populated. The method returns key and display value that match entered prefix (array of string that contains two items).
        /// </summary>
        /// <remarks>The signature of this method must match the following:
        /// <code>public string[] GetKeyAndDisplayValue(string prefixText, string contextKey)</code>
        /// Note that you can replace "GetKeyAndDisplayValue" with a name of your choice, but the return type and parameter names 
        /// and types must exactly match, including case.
        /// </remarks>
        [Bindable(false)]
        [Category("Behavior")]
        [DefaultValue(null)]
        [Description("Optional web service method to be called when a user types in a value prefix but doesn't wait for flyout list/menu to be populated.")]
        public string GetKeyAndDisplayValueServiceMethod
        {
            get { return this.getKeyAndDisplayValueServiceMethod; }
            set { this.getKeyAndDisplayValueServiceMethod = value; }
        }

        /// <summary>
        /// Gets or sets minimum number of characters that must be entered before getting suggestions from the web service.
        /// </summary>
        [Bindable(false)]
        [Category("Behavior")]
        [DefaultValue(1)]
        [Description("The web service method to be called when a user types some text and which returns word completions.")]
        public int MinimumPrefixLength
        {
            get { return this.uiAutoComplete.MinimumPrefixLength; }
            set { this.uiAutoComplete.MinimumPrefixLength = value; }
        }

        /// <summary>
        /// Gets or sets the number of suggestions to be retrieved from the web service.
        /// </summary>
        [Bindable(false)]
        [Category("Behavior")]
        [DefaultValue(1)]
        [Description("Number of suggestions to be retrieved from the web service.")]
        public int CompletionSetCount
        {
            get { return this.uiAutoComplete.CompletionSetCount; }
            set { this.uiAutoComplete.CompletionSetCount = value; }
        }

        /// <summary>
        /// Gets or sets CSS Class that will be used to style the completion list flyout.
        /// </summary>
        [Bindable(false)]
        [Category("Appearance")]
        [DefaultValue("")]
        [Description("CSS Class that will be used to style the completion list flyout.")]
        public string CompletionListCssClass
        {
            get { return this.uiAutoComplete.CompletionListCssClass; }
            set { this.uiAutoComplete.CompletionListCssClass = value; }
        }

        /// <summary>
        /// Gets or sets CSS Class that will be used to style an item in the AutoComplete list flyout.
        /// </summary>
        [Bindable(false)]
        [Category("Appearance")]
        [DefaultValue("")]
        [Description("CSS Class that will be used to style an item in the AutoComplete list flyout.")]
        public string CompletionListItemCssClass
        {
            get { return this.uiAutoComplete.CompletionListItemCssClass; }
            set { this.uiAutoComplete.CompletionListItemCssClass = value; }
        }

        /// <summary>
        /// Gets or sets CSS Class that will be used to style a highlighted item in the AutoComplete list flyout.
        /// </summary>
        [Bindable(false)]
        [Category("Appearance")]
        [DefaultValue("")]
        [Description("CSS Class that will be used to style a highlighted item in the AutoComplete list flyout.")]
        public string CompletionListHighlightedItemCssClass
        {
            get { return this.uiAutoComplete.CompletionListHighlightedItemCssClass; }
            set { this.uiAutoComplete.CompletionListHighlightedItemCssClass = value; }
        }

        #endregion
        
        /// <summary>
		/// Gets or sets the key value selected in the lookup, ie. flyout list/menu.
		/// </summary>
        [Bindable(true, BindingDirection.TwoWay)]
        [DefaultValue("")]
        [Description("Key value selected in the lookup, ie. flyout list/menu.")]
        public string SelectedValue
        {
            get { return this.hdnValue.Value; }
            set { this.hdnValue.Value = value; }
        }

        /// <summary>
        /// Gets or sets a value indicating whether the Web server control is enabled.
        /// </summary>
        [DefaultValue(true)]
        [Category("Behavior")]
        [Themeable(false)]
        [Bindable(true)]
        [Description("Indicates whether the Web server control is enabled.")]
        public override bool Enabled
        {
            get { return base.Enabled; }
            set
            {
                base.Enabled = value;
                this.imgClear.Disabled = !value;
            }
        }

        /// <summary>
        /// Gets or sets a value that indicates whether an automatic postback to the server occurs when the <see cref="SelectedValue"/> changes on the client, ie. whether a user clears or selects an item from a fly-out menu.
        /// </summary>
        [Themeable(false)]
        [Category("Behavior")]
        [DefaultValue(false)]
        [Description("Indicates whether an automatic postback to the server occurs when the selected value changes on the client, ie. whether a user clears or selects an item from a fly-out menu.")]
        public bool AutoPostBack { get; set; }

        /// <summary>
        /// Occurs when a user clears or selects lookup values in browser and <see cref="AutoPostBack"/> property is set to <b>true</b>.
        /// </summary>
        [Description("Occurs when a user clears or selects lookup values in browser. Requires that AutoPostBack is set to true.")]
        public event EventHandler ValueSelected
        {
            add { this.btnAutoPostBack.Click += value; }
            remove { this.btnAutoPostBack.Click -= value; }
        }

        /// <summary>Gets the control which contains the SelectedValue property.</summary>
        [Description("Control which contains the <b>SelectedValue</b> property.")]
        [Browsable(false)]
        [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
        public Control SelectedValueControl
        {
            get { return this.hdnValue; }
        }

        /// <summary>Gets the control which contains the <b>Text</b> property.</summary>
        [Description("Control which contains the Text property.")]
        [Browsable(false)]
        [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
        public Control TextControl
        {
            get { return this.txtText; }
        }

        #region TextBox properties.

        /// <summary>
        /// Gets or sets the text content (display value) of the control.
        /// </summary>
        [Bindable(true, BindingDirection.TwoWay)]
        [DefaultValue("")]
        [Description("Text content (display value) of the control.")]
        public string Text
        {
            get { return this.txtText.Text; }
            set { this.txtText.Text = value; }
        }

        /// <summary>
        /// Gets or sets the access key that allows you to quickly navigate to the Web server control.
        /// </summary>
        /// <value>The access key for quick navigation to the Web server control. The default value is Empty, which indicates that this property is not set.</value>
        [Category("Behavior")]
        [DefaultValue("")]
        [Description("Access key that allows you to quickly navigate to the Web server control.")]
        public override string AccessKey
        {
            get { return this.txtText.AccessKey; }
            set { this.txtText.AccessKey = value; }
        }

        /// <summary>
        /// Gets or sets the display width of the text box in characters.
        /// </summary>
        /// <value>The display width, in characters, of the text box. The default is 0, which indicates that the property is not set.</value>
        [Category("Appearance")]
        [DefaultValue(0)]
        [Description("Display width of the text box in characters.")]
        public int Columns
        {
            get { return this.txtText.Columns; }
            set { this.txtText.Columns = value; }
        }

        /// <summary>
        /// Gets or sets the width of the text box.
        /// </summary>
        [Category("Appearance")]
        [Description("Width of the control.")]
        public override Unit Width
        {
            get { return this.txtText.Width; }
            set { this.txtText.Width = value; }
        }

        /// <summary>
        /// Gets or sets the Cascading Style Sheet (CSS) class rendered by the Web server control on the client.
        /// </summary>
        [Bindable(false)]
        [Category("Appearance")]
        [DefaultValue(typeof(string), "")]
        [Description("Cascading Style Sheet (CSS) class rendered by the Web server control on the client.")]
        public override string CssClass
        {
            get { return this.txtText.CssClass; }
            set { this.txtText.CssClass = value; }
        }

        #endregion

        #region Read-only label properties.

        /// <summary>
        /// Gets or sets the Cascading Style Sheet (CSS) class rendered by the Web server control on the client 
        /// when the control is in read-only mode.
        /// </summary>
        [Bindable(false)]
        [Category("Appearance")]
        [DefaultValue(typeof(string), "")]
        [Description("Cascading Style Sheet (CSS) class rendered by the Web server control on the client when the control is in read-only mode.")]
        public string ReadOnlyCssClass
        {
            get { return this.lblText.CssClass; }
            set { this.lblText.CssClass = value; }
        }

        #endregion

        #region Clear image.

        /// <summary>
        /// Gets or sets a value that indicates whether the clear image is visible.
        /// </summary>
        [DefaultValue(true)]
        [Bindable(true)]
        [Category("Behavior")]
        [Description("Indicates whether the clear image is visible.")]
        public bool ClearImageVisible
        {
            get { return this.imgClear.Visible; }
            set { this.imgClear.Visible = value; }
        }

        /// <summary>
        /// Gets or sets the URL of the clear button image.
        /// </summary>
        [Bindable(false)]
        [Category("Appearance")]
        [Description("URL of the clear button image.")]
        [DefaultValue("")]
        public string ClearImageUrl
        {
            get { return this.imgClear.Src; }
            set { this.imgClear.Src = value; }
        }

        /// <summary>
        /// Gets or sets the Cascading Style Sheet (CSS) class applied to clear image.
        /// </summary>
        [Category("Appearance")]
        [CssClassProperty]
        [DefaultValue("")]
        [Description("CSS class applied to clear image.")]
        public string ClearImageCssClass
        {
            get { return this.imgClear.Attributes["class"] ?? ""; }
            set 
            {
                if (value == null)
                    this.imgClear.Attributes.Remove("class");
                else
                    this.imgClear.Attributes["class"] = value; 
            }
        }

        #endregion

        /// <summary>Gets the name of the control property which is bound two-way to a business object field: <b>SelectedValue</b></summary>
        [Browsable(false)]
        public string TwoWayBoundProperty
        {
            get { return "SelectedValue"; }
        }

        /// <summary>Gets the names of additional control properties which are bound one-way to business object properties: <b>Text</b></summary>
        [Browsable(false)]
        public string[] OneWayBoundProperties
        {
            get { return new string[] { "Text" }; }
        }

        /// <summary>Gets error label control which displays errors associated with the given field.</summary>
        [Browsable(false)]
        public ITextControl ErrorLabel
        {
            get { return this.lblError; }
        }

        /// <summary>Not implemented. Returns <b>null</b>.</summary>
        [Browsable(false)]
        ITextControl IBoundControl.CaptionLabel
        {
            get { return null; }
        }

        /// <summary>Not implemented. Returns <b>null</b>.</summary>
        [Browsable(false)]
        ITextControl IBoundControl.TooltipLabel
        {
            get { return null; }
        }

        /// <summary>Not implemented. Returns <b>null</b>.</summary>
        [Browsable(false)]
        Control IBoundControl.RequiredFieldIndicatorControl
        {
            get { return null; }
        }
    }
}
